/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.opengoofy.index12306.biz.payservice.service.impl;

import com.alibaba.fastjson2.JSON;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.opengoofy.index12306.biz.payservice.common.enums.TradeStatusEnum;
import org.opengoofy.index12306.biz.payservice.convert.RefundRequestConvert;
import org.opengoofy.index12306.biz.payservice.dao.entity.PayDO;
import org.opengoofy.index12306.biz.payservice.dao.mapper.PayMapper;
import org.opengoofy.index12306.biz.payservice.dto.PayCallbackReqDTO;
import org.opengoofy.index12306.biz.payservice.dto.PayInfoRespDTO;
import org.opengoofy.index12306.biz.payservice.dto.PayRespDTO;
import org.opengoofy.index12306.biz.payservice.dto.RefundCommand;
import org.opengoofy.index12306.biz.payservice.dto.RefundReqDTO;
import org.opengoofy.index12306.biz.payservice.dto.RefundRespDTO;
import org.opengoofy.index12306.biz.payservice.dto.base.PayRequest;
import org.opengoofy.index12306.biz.payservice.dto.base.PayResponse;
import org.opengoofy.index12306.biz.payservice.dto.base.RefundRequest;
import org.opengoofy.index12306.biz.payservice.dto.base.RefundResponse;
import org.opengoofy.index12306.biz.payservice.handler.AliPayNativeHandler;
import org.opengoofy.index12306.biz.payservice.handler.AliRefundNativeHandler;
import org.opengoofy.index12306.biz.payservice.mq.event.PayResultCallbackOrderEvent;
import org.opengoofy.index12306.biz.payservice.mq.produce.PayResultCallbackOrderSendProduce;
import org.opengoofy.index12306.biz.payservice.service.PayService;
import org.opengoofy.index12306.biz.payservice.service.payid.PayIdGeneratorManager;
import org.opengoofy.index12306.framework.starter.cache.DistributedCache;
import org.opengoofy.index12306.framework.starter.common.toolkit.BeanUtil;
import org.opengoofy.index12306.framework.starter.convention.exception.ServiceException;
import org.opengoofy.index12306.framework.starter.designpattern.strategy.AbstractStrategyChoose;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.math.BigDecimal;
import java.util.Objects;
import java.util.concurrent.TimeUnit;

import static org.opengoofy.index12306.biz.payservice.common.constant.RedisKeyConstant.ORDER_PAY_RESULT_INFO;

/**
 * 支付接口层实现
 * 公众号：马丁玩编程，回复：加群，添加马哥微信（备注：12306）获取项目资料
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class PayServiceImpl implements PayService {

    private final PayMapper payMapper;
    private final AbstractStrategyChoose abstractStrategyChoose;
    private final PayResultCallbackOrderSendProduce payResultCallbackOrderSendProduce;
    private final DistributedCache distributedCache;

    /**
     * 统一支付接口
     * 该方法用于处理所有支付请求，包括但不限于选择支付策略、生成支付订单、更新数据库和缓存结果
     * 使用策略模式动态选择支付组件，以适应不同的支付渠道和场景
     * 
     * @param requestParam 支付请求参数，包含订单信息、支付金额等必要信息
     * @return PayRespDTO 支付响应对象，包含支付结果、支付链接等信息
     * @throws ServiceException 如果支付订单创建失败，抛出业务异常
     */
    @Transactional(rollbackFor = Exception.class)
    @Override
    public PayRespDTO commonPay(PayRequest requestParam) {
        // 从分布式缓存中获取支付结果，以避免重复处理支付请求
        PayRespDTO cacheResult = distributedCache.get(ORDER_PAY_RESULT_INFO + requestParam.getOrderSn(), PayRespDTO.class);
        if (cacheResult != null) {
            return cacheResult;
        }
        /**
         * 使用策略模式选择并执行支付策略
         * {@link AliPayNativeHandler}
         */
        // 策略模式：通过策略模式封装支付渠道和支付场景，用户支付时动态选择对应的支付组件
        PayResponse result = abstractStrategyChoose.chooseAndExecuteResp(requestParam.buildMark(), requestParam);
        
        // 将支付请求参数转换为数据库保存的支付对象
        PayDO insertPay = BeanUtil.convert(requestParam, PayDO.class);
        // 生成支付单号
        String paySn = PayIdGeneratorManager.generateId(requestParam.getOrderSn());
        insertPay.setPaySn(paySn);
        // 设置支付状态为等待买家付款
        insertPay.setStatus(TradeStatusEnum.WAIT_BUYER_PAY.tradeCode());
        // 将支付金额转换为分，并保留整数
        insertPay.setTotalAmount(requestParam.getTotalAmount().multiply(new BigDecimal("100")).setScale(0, BigDecimal.ROUND_HALF_UP).intValue());
        
        // 插入支付记录到数据库
        int insert = payMapper.insert(insertPay);
        // 如果支付记录插入失败，抛出业务异常
        if (insert <= 0) {
            log.error("支付单创建失败，支付聚合根：{}", JSON.toJSONString(requestParam));
            throw new ServiceException("支付单创建失败");
        }
        
        // 将支付结果保存到分布式缓存中，设置缓存时间为10分钟
        distributedCache.put(ORDER_PAY_RESULT_INFO + requestParam.getOrderSn(), JSON.toJSONString(result), 10, TimeUnit.MINUTES);
        
        // 将支付响应对象转换为支付响应DTO并返回
        return BeanUtil.convert(result, PayRespDTO.class);
    }

    /**
     * 回调支付结果
     * <p>
     * 当接收到支付服务的回调通知时，此方法用于处理支付结果，包括查询支付单，更新支付单状态，以及在必要时向订单服务发送回调通知
     * </p>
     * 
     * @param requestParam 支付回调请求参数，包含支付服务返回的支付结果信息
     * @throws ServiceException 当支付单不存在或更新支付单支付结果失败时抛出此异常
     */
    @Transactional(rollbackFor = Exception.class)
    @Override
    public void callbackPay(PayCallbackReqDTO requestParam) {
        // 根据订单号查询支付单
        LambdaQueryWrapper<PayDO> queryWrapper = Wrappers.lambdaQuery(PayDO.class)
                .eq(PayDO::getOrderSn, requestParam.getOrderSn());
        PayDO payDO = payMapper.selectOne(queryWrapper);
        // 如果支付单不存在，记录错误日志并抛出异常
        if (Objects.isNull(payDO)) {
            log.error("支付单不存在，orderRequestId：{}", requestParam.getOrderRequestId());
            throw new ServiceException("支付单不存在");
        }
        // 更新支付单的信息，包括交易号、支付状态、支付金额和支付时间
        payDO.setTradeNo(requestParam.getTradeNo());
        payDO.setStatus(requestParam.getStatus());
        payDO.setPayAmount(requestParam.getPayAmount());
        payDO.setGmtPayment(requestParam.getGmtPayment());
        // 根据订单号更新支付单的支付信息
        LambdaUpdateWrapper<PayDO> updateWrapper = Wrappers.lambdaUpdate(PayDO.class)
                .eq(PayDO::getOrderSn, requestParam.getOrderSn());
        int result = payMapper.update(payDO, updateWrapper);
        // 如果更新支付单支付信息失败，记录错误日志并抛出异常
        if (result <= 0) {
            log.error("修改支付单支付结果失败，支付单信息：{}", JSON.toJSONString(payDO));
            throw new ServiceException("修改支付单支付结果失败");
        }
        // 如果支付状态为交易成功，回调订单服务告知支付结果，修改订单状态
        if (Objects.equals(requestParam.getStatus(), TradeStatusEnum.TRADE_SUCCESS.tradeCode())) {
            payResultCallbackOrderSendProduce.sendMessage(BeanUtil.convert(payDO, PayResultCallbackOrderEvent.class));
        }
    }

    /**
     * 根据订单号获取支付信息
     * 
     * 本方法主要用于查询特定订单的支付信息它通过订单号（orderSn）来查询支付相关数据，
     * 并将查询结果转换为PayInfoRespDTO对象返回这个方法是为了支持订单支付信息的查询，
     * 以便于前端或其他服务可以获取到订单的支付详情
     * 
     * @param orderSn 订单号，唯一标识一个订单
     * @return 返回支付信息响应对象，包含订单的支付详情
     */
    @Override
    public PayInfoRespDTO getPayInfoByOrderSn(String orderSn) {
        // 构建查询条件，查询PayDO表中订单号为orderSn的支付信息
        LambdaQueryWrapper<PayDO> queryWrapper = Wrappers.lambdaQuery(PayDO.class)
                .eq(PayDO::getOrderSn, orderSn);
        // 执行查询操作，获取支付信息
        PayDO payDO = payMapper.selectOne(queryWrapper);
        // 将查询结果转换为PayInfoRespDTO对象并返回
        return BeanUtil.convert(payDO, PayInfoRespDTO.class);
    }

    /**
     * 根据支付序列号获取支付信息
     * 
     * 本方法主要用于通过支付序列号（paySn）检索和获取相关的支付信息它首先构建一个查询条件，
     * 然后通过映射器（Mapper）查询数据库中匹配该条件的第一条支付记录如果找到相应的记录，
     * 则将其转换为PayInfoRespDTO对象返回这个过程涉及从数据库中加载数据以及对象的转换
     * 
     * @param paySn 支付序列号，是查询支付信息的唯一标识
     * @return 返回包含支付信息的PayInfoRespDTO对象如果找不到对应的支付记录，返回的将是null
     */
    @Override
    public PayInfoRespDTO getPayInfoByPaySn(String paySn) {
        // 构建查询条件，查询条件为支付序列号等于传入的paySn
        LambdaQueryWrapper<PayDO> queryWrapper = Wrappers.lambdaQuery(PayDO.class)
                .eq(PayDO::getPaySn, paySn);
        // 执行查询操作，获取第一条匹配记录
        PayDO payDO = payMapper.selectOne(queryWrapper);
        // 将查询到的PayDO对象转换为PayInfoRespDTO对象并返回
        return BeanUtil.convert(payDO, PayInfoRespDTO.class);
    }

    /**
     * 通用退款接口
     * 该方法处理退款请求，主要包括验证支付单的存在性、选择合适的退款策略并执行退款，
     * 以及更新支付单的退款状态
     * 
     * @param requestParam 退款请求参数，包含如订单号等信息
     * @return RefundRespDTO 返回退款响应DTO，目前返回值为null
     * @throws ServiceException 当支付单不存在或更新支付单退款结果失败时抛出服务异常
     */
    @Override
    public RefundRespDTO commonRefund(RefundReqDTO requestParam) {
        // 根据订单号查询支付单信息
        LambdaQueryWrapper<PayDO> queryWrapper = Wrappers.lambdaQuery(PayDO.class)
                .eq(PayDO::getOrderSn, requestParam.getOrderSn());
        PayDO payDO = payMapper.selectOne(queryWrapper);
        // 如果支付单不存在，则记录错误日志并抛出异常
        if (Objects.isNull(payDO)) {
            log.error("支付单不存在，orderSn：{}", requestParam.getOrderSn());
            throw new ServiceException("支付单不存在");
        }
        /**
         * {@link AliRefundNativeHandler}
         */
        // 将支付单信息转换为退款命令对象，并基于命令对象创建退款请求
        RefundCommand refundCommand = BeanUtil.convert(payDO, RefundCommand.class);
        RefundRequest refundRequest = RefundRequestConvert.command2RefundRequest(refundCommand);
        // 选择并执行相应的退款策略
        RefundResponse result = abstractStrategyChoose.chooseAndExecuteResp(refundRequest.buildMark(), refundRequest);
        // 更新支付单的退款状态
        payDO.setStatus(result.getStatus());
        // 准备更新支付单状态的条件包装器
        LambdaUpdateWrapper<PayDO> updateWrapper = Wrappers.lambdaUpdate(PayDO.class)
                .eq(PayDO::getOrderSn, requestParam.getOrderSn());
        // 执行更新操作
        int updateResult = payMapper.update(payDO, updateWrapper);
        // 如果更新失败，则记录错误日志并抛出异常
        if (updateResult <= 0) {
            log.error("修改支付单退款结果失败，支付单信息：{}", JSON.toJSONString(payDO));
            throw new ServiceException("修改支付单退款结果失败");
        }
        return null;
    }
}
